package wikibooks;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Converts Strings of MediaWiki markup into LaTeX based on
 * <code>ConversionSettings settings</code>. Instantiate with
 * <code>new WikicodeConverterImpl(ConversionSettings settings)</code> and convert
 * with <code>convert(String wikicode)</code>, which returns Strings of LaTeX
 * markup, or use any other public method.
 * 
 * <p>Invocation of this class is thread-safe due (assumming that reading from
 * ConversionSettings and BookSettings and writing to Java logger are safe).
 * Since method convert() does not
 * change anything sensitive in internal state of the object, you can use
 * the same object many times to convert completely different data.</p>
 * 
 * <p>Order of work:
 * <ol>
 * <li>convert primary sections</li>
 * <li>convert block sections</li>
 * <li>convert secondary sections</li>
 * <li>remove LaTeX special characters</li>
 * <li>convert inline formatting</li>
 * <li>final removal of the rest of LaTeX special characters</li>
 * <li>final cosmetics</li>
 * </ol>
 * </p>
 */
public class WikicodeConverterImpl implements WikicodeConverter {

    private ConversionSettings settings;
    private BookSettings bs;
    private String logPrefix = "";
    private Logger logger;
    private boolean isInsideTable = false;
    private static final String URL_MASK_PREFIX = "LINK";

    /**
     * Create converter with default settings.
     */
    public WikicodeConverterImpl() {
        settings = new ConversionSettings();
        logger = Logger.getLogger("WikicodeConverter");
    }

    public WikicodeConverterImpl(BookSettings bs, ConversionSettings settings) {
        this.bs = bs;
        this.settings = settings;
        logger = Logger.getLogger("WikicodeConverter");
    }

    /**
     * creates WikicodeConverterImpl.
     * @param bs book settings
     * @param settings conversion settings
     * @param wikicodeName name associated with converted wikicode (eg. chapter name)
     */
    public WikicodeConverterImpl(BookSettings bs, ConversionSettings settings,
            String wikicodeName) {
        this.bs = bs;
        this.settings = settings;
        setWikicodeName(wikicodeName);
        logger = Logger.getLogger("WikicodeConverter");
    }

    /**
     * sets wikicode name 
     * @param name associated with converted wikicode (eg. chapter name)
     */
    public void setWikicodeName(String name) {
        if (name.equals("")) {
            logPrefix = "";
        } else {
            logPrefix = name + " ";
        }
    }

    /**
     * hides sections starting with <em>startTag</em> and ending with <em>endTag</em>
     * @param wikicode wikicode to mask
     * @param masked masked elements are remembered here (output parameter)
     * @param startTag start tag of masked section
     * @param endTag end tag of masked section
     * @param maskPrefix unique string added to each masked element
     * (each method invoking this function should have separate maskPrefix)
     * @return wikicode with sections replaced by non-wikicode text
     */
    String maskSections(String wikicode, ArrayList<String> masked, final String startTag, final String endTag,
            final String maskPrefix) {
        for (int i = 0; i < 500; ++i) {
            if (wikicode.contains(startTag)) {
                int start = wikicode.indexOf(startTag);
                int end = wikicode.indexOf(endTag, start);
                if (start == -1 || end == -1) {
                    break;
                }
                masked.add(wikicode.substring(start, end + endTag.length()));
                wikicode = wikicode.substring(0, start) + maskPrefix + i + wikicode.substring(end + endTag.length());
            } else {
                break;
            }
        }
        return wikicode;
    }

    /**
     * Restores sections masked by maskSections() function.
     * @param wikicode wikicode changed by maskSections() method
     * @param masked array outputted by maskSections() method
     * @param maskPrefix mask prefix passed to maskSections() method
     * @return wikicode with restored sections
     */
    String unmaskSections(String wikicode, ArrayList<String> masked, final String maskPrefix) {
        for (int i = 0; i < masked.size(); ++i) {
            wikicode = wikicode.replace(maskPrefix + i, masked.get(i));
        }
        return wikicode;
    }

    /**
     * Converts MediaWiki markup in String wikicode to LaTeX and returns LaTeX.
     * 
     * @param wikicode
     *            MediaWiki markup.
     * @return LaTeX markup.
     */
    public String convert(String wikicode) {
        
        // standardize syntax
        wikicode = wikicode.replaceAll("\n<math>(.*)</math>\\s*\n", "\n: <math>$1</math>\n");
        wikicode = wikicode.replace("\n:<math>", "\n: <math>");
        wikicode = wikicode.replaceAll("\n: <math>(.*?)</math>([\t ]*\\S.*)\n", "\n<center><math>$1</math>$2</center>\n");
        wikicode = wikicode.replaceAll("\n: <math>(.*)</math>", "\n<standalone-math>$1</standalone-math>");
        wikicode = wikicode.replace("[[image:", "[[Image:");
        wikicode = wikicode.replace("[[category:", "[[Category:");
        wikicode = wikicode.replaceAll("\\[\\[(g|G)rafika:", "[[Image:");
        wikicode = wikicode.replaceAll("\\[\\[(k|K)ategoria:", "[[Category:");

        //ArrayList<String> nowiki = new ArrayList<String>();
        //wikicode = maskSections(wikicode, nowiki, "<nowiki>", "</nowiki>", "nowikisection");

        // temporary preprocessing
        //wikicode = wikicode.replaceAll("\\{\\{samp(Err)?\\s*\\|(1=)?(([^\\}]|\\s)+)\\}\\}", "$3");

        //wikicode = unmaskSections(wikicode, nowiki, "nowikisection");

        // convert
        String latex = convert2(wikicode);

        // remove extra newlines
        while (latex.contains("\n\n\n")) {
            latex = latex.replace("\n\n\n", "\n\n");
        }
        while (latex.length() > 0 && latex.charAt(latex.length() - 1) == '\n') {
            latex = latex.substring(0, latex.length() - 1);
        }

        return latex;
    }

    // Start private helper methods (public special-purpose methods below)
    private String convert2(String wikicode) {
        String wikicodeWithConvertedSections;

        wikicode = wikicode.replace("\r\n", "\n");


        // convert sections between open and cloes tags
        wikicodeWithConvertedSections = convertPrimarySections(wikicode);
        if (wikicodeWithConvertedSections != null) {
            return wikicodeWithConvertedSections;
        }

        // convert lists, indented text, and indented preformatted text
        wikicodeWithConvertedSections = convertBlockSections(wikicode);
        if (wikicodeWithConvertedSections != null) {
            return wikicodeWithConvertedSections;
        }

        wikicodeWithConvertedSections = convertSecondarySections(wikicode);
        if (wikicodeWithConvertedSections != null) {
            return wikicodeWithConvertedSections;
        }

        // convert everything else
        return convertInlineFormatting(wikicode, false);
    }
    /** sections processed before block elements.
     * Values: open tag, close tag, [open tag conflicting prefix, close tag conflicting prefix] (optional)
     */
    private final static String[][] PRIMARY_SECTIONS = {
        {"<!--", "-->"}, // comments
        {"<nowiki>", "</nowiki>"},
        {"<includeonly>", "</includeonly>"},
        {"<noinclude>", "</noinclude>"},
        {"<source lang=\"", "</source>"},
        {"<pre", "</pre>"}, // preformatted text
        {"<code", "</code>"},
        {"<gallery>", "</gallery>"},
        {"<standalone-math>", "</standalone-math>"},
        {"<math>", "</math>"},};
    /** sections processed after block elements but before inline formatting. */
    private final static String[][] SECONDARY_SECTIONS = {
        {"{|", "|}"}, // tables
        {"[[Image:", "]]", "[[", "]]"},
        {"[[Category:", "]]"},
        {"[[Kategoria:", "]]"},
        {"[[Kategorie:", "]]"},
        {"{{", "}}"}, // templates
        {"<tt>", "</tt>"},
        {"<sup>", "</sup>"},
        {"<center>", "</center>"},};

    private String convertPrimarySections(String wikicode) {
        return convertSections(wikicode, PRIMARY_SECTIONS);
    }

    private String convertSecondarySections(String wikicode) {
        return convertSections(wikicode, SECONDARY_SECTIONS);
    }

    private String convertSections(String wikicode, final String[][] tagPairs) {
        for (String[] tagPair : tagPairs) {
            final String confOpenPrefix = tagPair.length > 2 ? tagPair[2] : "";
            final String confClosePrefix = tagPair.length > 3 ? tagPair[3] : "";

            if (matches(wikicode, tagPair[0], tagPair[1], confOpenPrefix, confClosePrefix)) {
                return convertSection(wikicode, tagPair[0], tagPair[1], confOpenPrefix, confClosePrefix);
            }
        }

        return null;
    }

    private String convertSection(String wikicode, final String openTag, final String closeTag,
            final String conflictingOpenTagPrefix, final String conflictingCloseTagPrefix) {

        if (!matches(wikicode, openTag, closeTag, conflictingOpenTagPrefix, conflictingCloseTagPrefix)) {
            Log.addError(logPrefix + "Could not convert:\n\"" + wikicode + "\".");
            return "% The below code was not converted due to an error.\n" + wikicode
                    + (wikicode.endsWith("\n") ? "" : "\n") + "% The above code was not converted due to an error.";
        }

        int openIndex, closeIndex;
        {
            int[] tempInd = tagIndices(wikicode, openTag, closeTag, conflictingOpenTagPrefix,
                    conflictingCloseTagPrefix);
            assert (tempInd.length == 2);
            openIndex = tempInd[0];
            closeIndex = tempInd[1];
        }

        if (openIndex == -1 || closeIndex == -1 || openIndex + openTag.length() > closeIndex) {
            logger.info(String.format("%scannot convert section; " +
                    "open tag %s, openIndex %d", logPrefix, openTag, openIndex));
            return wikicode;
        }

        String preText = wikicode.substring(0, openIndex);
        String postText = wikicode.substring(closeIndex + closeTag.length());

        String sectionText = wikicode.substring(openIndex + openTag.length(), closeIndex);

        /*
         * The wikicode surrounding the section (String sectionText) is
         * converted, with the section temporarily represented by a unique
         * number. This is done so code like "'''foo <!-- bar ---> baz'''" is
         * correctly converted.
         */
        String sectionNumber = generateSectionNumber(openTag);
        String latex = convert2(preText + sectionNumber + postText);

        sectionText = trim(sectionText);

        // convert section
        if (openTag.equals("<pre")) {
            sectionText = sectionText.substring(sectionText.indexOf(">") + 1);
            sectionText = convertPreformattedText(sectionText);
        } else if (openTag.equals("<nowiki>")) {
            sectionText = convertNowikiSection(sectionText);
        } else if (openTag.equals("<noinclude>")) {
            sectionText = convertNoinclude(sectionText);
        } else if (openTag.equals("<includeonly>")) {
            sectionText = convertIncludeonly(sectionText);
        } else if (openTag.equals("{{")) {
            sectionText = convertTemplate("{{" + sectionText + "}}");
        } else if (openTag.equals("<code")) {
            sectionText = sectionText.substring(sectionText.indexOf(">") + 1);
            sectionText = convertCode(sectionText);
        } else if (openTag.equals("<gallery>")) {
            sectionText = convertGallery(sectionText);
        } else if (openTag.startsWith("<!-")) {
            sectionText = convertComment(sectionText);
        } else if (openTag.equals("<standalone-math>")) {
            sectionText = convertStandAloneMath(sectionText);
        } else if (openTag.equals("<math>")) {
            sectionText = convertInlineMath(sectionText);
        } else if (openTag.matches("\\[\\[(Image|Grafika|Bild):")) {
            sectionText = convertImage(sectionText);
        } else if (openTag.matches("\\[\\[(Category|Kategoria|Kategorie):")) {
            sectionText = "";  // remove categories
        } else if (openTag.equals("{|")) {
            sectionText = convertTable(sectionText);
        } else if (openTag.equals("<source lang=\"")) {
            String language = sectionText.substring(0, sectionText.indexOf("\">"));
            sectionText = sectionText.substring(sectionText.indexOf("\">") + 2);
            sectionText = convertSource(sectionText, language);
        } else if (openTag.equals("<tt>")) {
            sectionText = convertTeletype(sectionText);
        } else if (openTag.equals("<sup>")) {
            sectionText = String.format("\\(^{%s}\\)", convert2(sectionText));
        } else if (openTag.equals("<center>")) {
            sectionText = convertCenter(sectionText);
        } else {
            logger.warning(logPrefix + "unknown opening tag: " + openTag);
        }

        return latex.replace(sectionNumber, sectionText);
    }

    /** Last used section id */
    private static int lastId = 0;

    /**
     * Returns a unique section ID based on its opening tag, so that the type of the section
     * is known and its name does not conflict with the tag itself.
     * @param openTag the opening tag of the section
     * @return the section type identifier (uppercase letters)
     */
    private static synchronized String generateSectionNumber(String openTag) {

        String sectionType = "";

        if (openTag.equals("{{")){
            sectionType = "TEMPLATE";
        }
        else if(openTag.equals("{|")){
            sectionType = "TABLE";
        }
        else if(openTag.equals("*") || openTag.equals("#") || openTag.equals(";")){
            sectionType = "LIST";
        }
        else if (openTag.equals(" ")){
            sectionType = "PRE";
        }
        else if (openTag.equals(":")){
            sectionType = "INDENTED";
        }
        else if(openTag.equals("<!--")){
            sectionType = "COMMENT";
        }
        else {
            sectionType = openTag.replaceAll("[^A-Za-z0-9]", "").toUpperCase();
        }
        return "(Section-" + sectionType + " " + lastId++ + ")";
    }

    /**
     * Returns true if the given string contains a protected section (see {@link #generateSectionNumber(String)})
     * which needs to be wrappable in a table.
     * @param str the string to be examined, e.g. a table cell contents
     * @return true if the string contains a section to be wrapped in a table
     */
    private boolean containsWrapSection(String str){
        if (str.matches(".*\\(Section-(TABLE|LIST|SOURCELANG|PRE|INDENTED) 0.*")){
            return true;
        }
        return false;
    }

    private String convertBlockSections(String wikicode) {
        final char[] firstCharsOnEachLine = {
            '*', // unordered lists
            '#', // ordered lists
            ';', // description lists
            ' ', // indented preformatted text
            ':', // indented text
        };

        for (char firstChar : firstCharsOnEachLine) {
            if (wikicode.startsWith("" + firstChar) || wikicode.contains("\n" + firstChar)) {
                return convertBlockSection(wikicode, firstChar);
            }
        }

        return null;
    }

    private String convertBlockSection(String wikicode, char firstChar) {
        int index = (wikicode.startsWith("" + firstChar)) ? 0 : wikicode.indexOf("\n" + firstChar) + 1;
        if (index == -1) {
            Log.addError(logPrefix + "Could not convert:\n\"" + wikicode + "\".");
            return "% The below code was not converted due to an error.\n" + wikicode
                    + (wikicode.endsWith("\n") ? "" : "\n") + "% The above code was not converted due to an error.";
        }

        String[] sections = blockSections(wikicode, index);

        /*
         * The wikicode surrounding the list (String listText) is converted,
         * with the list temporarily represented by a unique number. This is
         * done so code like "<div> * list item </div>" is correctly
         * converted.
         */
        String sectionNumber = generateSectionNumber(""+firstChar);
        String latex = convert2(sections[0] + sectionNumber + sections[2]);

        if (firstChar == '*' || firstChar == '#' || firstChar == ';') {
            sections[1] = convertList(sections[1]);
        } else if (firstChar == ' ') {
            sections[1] = convertIndentedPreformattedText(sections[1]);
        } else if (firstChar == ':') {
            sections[1] = convertIndentedText(sections[1]);
        }

        return latex.replace(sectionNumber, sections[1]);
    }

    private String[] blockSections(String wikicode, int listIndex) {
        char listType = wikicode.charAt(listIndex);

        String preText = wikicode.substring(0, listIndex);
        String postText = "\n";
        String blockText = "";

        boolean inBlock = true;
        String restOfWikicode = wikicode.substring(listIndex);
        for (String line : Util.lineArray(restOfWikicode)) {
            if (inBlock && (line.startsWith("" + listType) || (listType == ';' && line.startsWith(":")))) {
                blockText += line + "\n";
            } else {
                inBlock = false;
                postText += line + "\n";
            }
        }

        // if block ends with }}, move }} to postText
        Pattern p = Pattern.compile("\\}\\}\\s*$");
        Matcher m = p.matcher(blockText);
        if (m.find()) {
            blockText = m.replaceAll("");
            postText += "\n}}";
        }

        return new String[]{preText, blockText, postText};
    }

    /**
     * removes whitespaces and empty lines from the beginning and end of the string.
     * @param text string to trim
     * @return trimmed string
     */
    private String trim(String text) {
        text = text.trim();

        while (text.endsWith("\n")) { //  || text.endsWith("-")
            text = text.substring(0, text.length() - 1);
        }
        while (text.startsWith("\n")) { // || text.startsWith("-")
            text = text.substring(1);
        }

        return text.trim();
    }

    private String trimSection(String wikicode, String openTag, String closeTag) {
        if (wikicode == null) {
            return null;
        }

        /*if (openTag != null
        && closeTag != null
        && matches(wikicode, openTag, closeTag))
        wikicode = wikicode.substring(wikicode.indexOf(openTag)
        + openTag.length(), wikicode.indexOf(closeTag));*/
        if (openTag != null && closeTag != null) {
            int[] indices = tagIndices(wikicode, openTag, closeTag);
            if (indices[0] >= 0 && indices[1] >= 0) {
                wikicode = wikicode.substring(indices[0] + openTag.length(),
                        indices[1]);
            }
        }

        return trim(wikicode);
    }

    /**
     * Checks if a matching String closeTag exists for String openTag in String
     * wikicode. Note that false is returned if String wikicode does not contain
     * openTag.
     * 
     * @param wikicode
     *            A line (or several lines) of mediawiki markup.
     * @param openTag
     *            The opening tag of a pair. For example: "&lt;div>", "{|", "[["
     * @param closeTag
     *            The closing tag of a pair. For example: "&lt;/div>", "|}",
     *            "]]"
     * @param conflictingOpenTagPrefix
     *            Prefix of open tag which may occur as part of completely another
     *            tag.
     * @param conflictingCloseTagPrefix
     *            Prefix of close tag which may occur as part of completely another
     *            tag.
     * @return true if String wikicode has a String closeTag to match String
     *         openTag
     */
    private boolean matches(String wikicode, String openTag, String closeTag, String conflictingOpenTagPrefix,
            String conflictingCloseTagPrefix) {
        // if (!wikicode.matches(".*\\Q" + openTag + "\\E(.*\n)*.*\\Q" + closeTag + "\\E.*"))
        if (wikicode == null || openTag == null || closeTag == null) {
            return false;
        }

        //int openIndex = wikicode.indexOf(openTag);
        //int closeIndex = wikicode.indexOf(closeTag, openIndex + 1);
        int[] indices = tagIndices(wikicode, openTag, closeTag,
                conflictingOpenTagPrefix, conflictingCloseTagPrefix);
        assert indices.length == 2;

        if (indices[0] != -1 && indices[1] == -1) {
            int firstIndex = indices[0] - 10;
            if (firstIndex < 0) {
                firstIndex = 0;
            }
            int lastIndex = indices[0] + openTag.length() + 10;
            if (lastIndex > wikicode.length()) {
                lastIndex = indices[0] + openTag.length();
            }
            logger.warning(String.format("%sno matching closing tag for %s ('%s')",
                    logPrefix, openTag, wikicode.substring(firstIndex, lastIndex)));
        }

        return indices[0] != -1 && indices[1] != -1;

    //int openTagLength = openTag.length();
    //int closeTagLength = closeTag.length();

    /*
     * Ensure that the closing container tag found at closeIndex is paired
     * with the opening container tag found at openIndex, and not a nested
     * pair of identical tags.
     */

    // to do

    /*int closeTagNumber = 1;
    String afterLastOpenTag = wikicode.substring(openIndex + openTagLength);
    String betweenTags = afterLastOpenTag.substring(0, closeIndex);
    if (betweenTags.contains(openTag)) {
    int lastOpenIndex = afterLastOpenTag.indexOf(openTag);
    afterLastOpenTag = afterLastOpenTag.substring(lastOpenIndex + openTagLength);
    closeTagNumber++;
    }*/

    // wikicode.matches(".*\\Q" + openTag + "\\E(.*\n)*.*\\Q" + closeTag + "\\E.*")

    //return openIndex > -1 && closeIndex > openIndex + openTag.length();
    }

    /**
     * counts number of occurances of substring in string.
     * @param string whole text to search in
     * @param substring pattern to find
     * @return number of disjoint occurances of substring in string (this means,
     * if two occurances of substring have common part, they are counted as
     * only one occurance)
     */
    int countOccurances(final String string, final String substring) {
        int count = 0;
        int matchStart = 0;
        final int subLen = substring.length();
        while (true) {
            int position = string.indexOf(substring, matchStart);
            if (position == -1) {
                return count;
            }

            ++count;
            matchStart = position + subLen;
        }
    }

    int[] tagIndices(String wikicode, String openTag, String closeTag) {
        return tagIndices(wikicode, openTag, closeTag, "", "");
    }

    /**
     * <p>
     * Given a line, adds the index of the first occurence of String openTag in
     * wikicode to int[0] and the index of its matching String closeTag to
     * int[1].
     * </p>
     * 
     * <p>
     * The purpose of this is to find the correct close tag when many are
     * possible. For example: "[[Image:foo.jpg|My [[Image]].]]", "&lt;div
     * class="outer"> &lt;div> class="inner"> ... &lt;/div> &lt;/div>"
     * </p>
     * 
     * @param wikicode
     *            A line (or several lines) of mediawiki markup.
     * @param openTag
     *            The opening tag of a pair. For example: "&lt;div>", "{|", "[["
     * @param closeTag
     *            The closing tag of a pair. For example: "&lt;/div>", "|}",
     *            "]]"
     * @param conflictingOpenTagPrefix
     *            Prefix of open tag which may occur as part of completely another
     *            tag (can be empty if such situation is impossible)
     * @param conflictingCloseTagPrefix
     *            Prefix of close tag which may occur as part of completely another
     *            tag (can be empty if such situation is impossible)
     * @return array of [openIndex,closeIndex]; note that any of them may be
     * -1 if open/close tag is not found
     */
    int[] tagIndices(String wikicode, String openTag, String closeTag,
            String conflictingOpenTagPrefix, String conflictingCloseTagPrefix) {
        int openIndex = wikicode.indexOf(openTag);
        int closeIndex = -1;

        if (conflictingOpenTagPrefix.equals("")) {
            conflictingOpenTagPrefix = openTag;
        }
        if (conflictingCloseTagPrefix.equals("")) {
            conflictingCloseTagPrefix = closeTag;
        }

        if (openIndex < 0) {
            return new int[]{openIndex, closeIndex};
        }

        int searchStart = openIndex + openTag.length();
        // to do
        while (true) {
            closeIndex = wikicode.indexOf(closeTag, searchStart);

            if (closeIndex < 0) {
                break;
            }

            String substr = wikicode.substring(openIndex + openTag.length(), closeIndex);
            if (countOccurances(substr, conflictingOpenTagPrefix) != countOccurances(substr, conflictingCloseTagPrefix)) {
                searchStart = closeIndex + closeTag.length();
            } else {
                break;
            }
        }

        return new int[]{openIndex, closeIndex};
    }

    private String convertLaTeXSpecialCharacters(String wikicode) {
        wikicode = wikicode.replace("{", "`;{").replace("}", "`;}").replace("\\", "\\textbackslash{}");
        wikicode = wikicode.replace("#", "\\#").replace("$", "\\$").replace("%", "\\%");
        wikicode = wikicode.replace("&le;", "$ \\leq $").replace("&ge;", "$ \\geq $");
        wikicode = wikicode.replace("&", "\\&").replace("_", "\\_").replace("π", "$\\pi{}$");
        wikicode = wikicode.replace("`;{", "\\{").replace("`;}", "\\}");
        return wikicode;
    }

    /**
     * escapes characters that have special meaning in LaTeX; run after all
     * other processing.
     */
    private String finalConvertLaTeXSpecialCharacters(String wikicode) {

        wikicode = wikicode.replace("^", "\\^{}").replace("<", "\\textless{}").replace(">", "\\textgreater{}"); // was: \\(<\\)
        wikicode = wikicode.replace("|", "\\textbar{}").replace("~", "\\~{}");
        return wikicode;
    }

    /**
     * makes cosmetic changes to final LaTeX code and removes some illegal
     * markup combinations.
     */
    private String finalCosmetics(String wikicode) {
        //wikicode = wikicode.replaceAll("\\\\(textbf|emph)\\{(\\\\verb[^\\}]+)\\}", "$2");
        
        wikicode = wikicode.replace(" i.e. ", " i.e.\\ ").replace(" e.g. ", " e.g.\\ ");
        
        return wikicode;
    }

    // End private helper methods
    // Start public special-purpose methods
    
    /**
     * converts inline formatting
     * @param wikicode wikicode
     * @param insideTeletype true if converting text inside \texttt{}
     */
    String convertInlineFormatting(String wikicode, boolean insideTeletype) {
        if (wikicode == null || wikicode.length() == 0) {
            return "";
        }
        int begin, end;

        wikicode = convertLaTeXSpecialCharacters(wikicode);

        // nbsp
        // use nbsp = "\ " or "\hspace*{5pt}" to retain a space for each &nbsp;
        if (settings.nbsp != null) {
            wikicode = wikicode.replace("\\&nbsp;", settings.nbsp);
        } else { // replace mutliple &nbsp;s with a single space
            wikicode = wikicode.replace("\\&nbsp;", " ");
            while (wikicode.indexOf("  ") != -1) {
                wikicode = wikicode.replace("  ", " ");
            }
        }

        wikicode = wikicode.replaceAll("\\n-{4,}\\n", "\n\\\\hrulefill\n");

        wikicode = wikicode.replace("\\&emdash;", "---");
        wikicode = wikicode.replace("\\&mdash;", "---");
        wikicode = wikicode.replace(" - ", " --- ");

        wikicode = wikicode.replace("\\&endash;", "--");
        wikicode = wikicode.replace("\\&ndash;", "--");

        wikicode = wikicode.replace("<ref>", "\\footnote{");
        wikicode = wikicode.replace("</ref>", "}");

        // TODO needs better numbering
        wikicode = wikicode.replaceAll("<ref\\s+name=\"?[^\"]+\"?\\s*/>", "\\\\footnotemark[1]");
        wikicode = wikicode.replaceAll("<ref\\s+name=\"?[^\"]+\"?\\s*>", "\\\\footnote[1]{");

        wikicode = wikicode.replace("<u>", "\\underline{");
        wikicode = wikicode.replace("</u>", "}");

        wikicode = wikicode.replace("<b>", "\\textbf{");
        wikicode = wikicode.replace("</b>", "}");

        wikicode = wikicode.replace("<i>", "\\emph{");
        wikicode = wikicode.replace("</i>", "}");

        wikicode = wikicode.replace("<s>", "\\sout{");
        wikicode = wikicode.replace("</s>", "}");

        wikicode = wikicode.replace("<br>", "\\\\"); // TODO handle differently at line beginning
        wikicode = wikicode.replace("<br/>", "\\\\");
        wikicode = wikicode.replace("<br />", "\\\\");

        wikicode = wikicode.replace("<sub>", "\\(_{");
        wikicode = wikicode.replace("</sub>", "}\\)");

        wikicode = wikicode.replace("<big>", "\\large{");
        wikicode = wikicode.replace("</big>", "}");
        
        wikicode = wikicode.replaceAll("<font[^>]*>","");
        wikicode = wikicode.replace("</font>", "");

        wikicode = wikicode.replace("<small>", "\\small{");
        wikicode = wikicode.replace("</small>", "}");

        wikicode = wikicode.replace("\\_\\_TOC\\_\\_", "");
        wikicode = wikicode.replace("\\_\\_NOTOC\\_\\_", "");

        wikicode = wikicode.replace("<references />", "");
        wikicode = wikicode.replace("<references/>", "");

        wikicode = wikicode.replace("<noinclude>", "");
        wikicode = wikicode.replace("</noinclude>", "");

        wikicode = wikicode.replace("<blockquote>", "\\begin{quote}");
        wikicode = wikicode.replace("</blockquote>", "\\end{quote}");

        // <onlyinclude>
        begin = wikicode.indexOf("<onlyinclude>");
        end = wikicode.indexOf("</onlyinclude>", begin);
        while (begin != -1 && end != -1) {
            wikicode = wikicode.substring(0, begin) + wikicode.substring(end + 14);
            begin = wikicode.indexOf("<onlyinclude>");
            end = wikicode.indexOf("</onlyinclude>", begin);
        }

        // span
        ConversionSettings.SpanAction spanAction = settings.spanAction;
        // <span>
        begin = wikicode.indexOf("<span");
        end = wikicode.indexOf(">", begin);
        while (begin != -1 && end != -1) {
            if (spanAction.equals(ConversionSettings.SpanAction.remove)) {
                wikicode = wikicode.substring(0, begin) + wikicode.substring(end + 1);
            } else if (spanAction.equals(ConversionSettings.SpanAction.ignore)) {
                break;
            } else { // comment out span
                wikicode = wikicode.substring(0, begin) + "\n%" + wikicode.substring(begin, end + 1) + "\n" + wikicode.substring(end + 1);
            }
            begin = wikicode.indexOf("<span", begin + 4);
            end = wikicode.indexOf(">", begin);
        }

        // </span>
        if (spanAction.equals(ConversionSettings.SpanAction.remove)) {
            wikicode = wikicode.replace("</span>", "");
        } else if (spanAction.equals(ConversionSettings.SpanAction.ignore)) {
            // ignore
        } else {
            wikicode = wikicode.replace("</span>", "\n%</span>\n");
        }

        // div
        ConversionSettings.DivAction divAction = settings.divAction;
        // <div>
        begin = wikicode.indexOf("<div");
        end = wikicode.indexOf(">", begin) + 1;
        while (begin != -1 && end > 0) {
            if (divAction.equals(ConversionSettings.DivAction.remove)) {
                wikicode = wikicode.substring(0, begin) + wikicode.substring(end);
            } else if (divAction.equals(ConversionSettings.DivAction.ignore)) {
                break;
            } else if (divAction.equals(ConversionSettings.DivAction.commentOut)) {
                wikicode = wikicode.substring(0, begin) + "\n%" + wikicode.substring(begin, end) + "\n" + wikicode.substring(end);
            } else // convert to box
            {
                wikicode = wikicode.substring(0, begin) + "\\fbox{" + wikicode.substring(end);
            }
            begin = wikicode.indexOf("<div", begin + 4);
            end = wikicode.indexOf(">", begin) + 1;
        }

        // </div>
        if (divAction.equals(ConversionSettings.DivAction.remove)) {
            wikicode = wikicode.replace("</div>", "");
        } else if (divAction.equals(ConversionSettings.DivAction.ignore)) {
            // ignore
        } else if (divAction.equals(ConversionSettings.DivAction.commentOut)) {
            wikicode = wikicode.replace("</div>", "\n%</div>\n");
        } else // convert to box
        {
            wikicode = wikicode.replace("</div>", "}");
        }

        // bold
        String command = settings.boldCommand();
        if (insideTeletype) {
            command = "";
        }
        wikicode = translateTag(wikicode, "\'\'\'", "\'\'\'", command);

        // emphasis           
        command = "\\emph";
        if (insideTeletype) {
            command = "";
        }
        wikicode = translateTag(wikicode, "\'\'", "\'\'", command);

        if (!insideTeletype) {
            // quotes (must come after italics)
            begin = wikicode.indexOf("\"");
            end = wikicode.indexOf("\"", begin + 1);
            // different language have different style quotes
            final String beginQuote = bs.getLanguage().equals("pl") ? ",," : "``";
            while (begin != -1 && end != -1) {
                wikicode = wikicode.substring(0, begin) + beginQuote + wikicode.substring(begin + 1, end) + "''" +
                        wikicode.substring(end + 1);

                begin = wikicode.indexOf("\"");
                end = wikicode.indexOf("\"", begin + 1);
            }

            // headings -- TODO heed "=" inside headings !!!
            
            for (int lvl=5; lvl>=1; --lvl) {
                Pattern p = Pattern.compile(String.format("(^|\n)={%s}([^=]+)={%s} *", lvl, lvl));
                while(true) {
                    Matcher m = p.matcher(wikicode);
                    if (!m.find()) {
                        break;
                    }
                    String replacement = m.group(1) + getOpenCodeForHeading(lvl) + m.group(2).trim() +"}";
                    final String tempRepl = "__R0__";
                    wikicode = m.replaceFirst(tempRepl);
                    wikicode = wikicode.replace(tempRepl, replacement);
                }
            }
            
        }
        
        wikicode = convertInternalLinks(wikicode);

        Map<String,String> maskedUrls = new HashMap<String, String>();
        wikicode = convertExternalLinks(wikicode, maskedUrls);

        wikicode = convertHTMLEntities(wikicode);
        wikicode = finalConvertLaTeXSpecialCharacters(wikicode);

        wikicode = finalCosmetics(wikicode);
        
        wikicode = unmaskUrls(wikicode, maskedUrls);

        return wikicode;
    }

    /**
     * returns LaTeX opening code of given level,
     * taking into account book settings which may modify the level.
     * @param level from 1 (MediaWiki's heading =) to 5 (heading =====)
     * @return opening code (like <code>\paragraph{</code>)
     */
    String getOpenCodeForHeading(int level) {
        level += settings.headingLevelModifier;

        if (level < 1) {
            //logger.warning(logPrefix+"heading level too small: "+level);
            level = 1;
        }
        if (level > 6) {
            //logger.warning(logPrefix+"heading level too big: "+level);
            level = 6;
        }

        switch (level) {
            case 1:
                return "\\chapter{";
            case 2:
                return "\\section{";
            case 3:
                return "\\subsection{";
            case 4:
                return "\\subsubsection{";
            case 5:
                return "\\paragraph{";
            case 6:
                return "\\subparagraph{";
            default:
                return "\\subsection{";
            }
    }
    
    /**
     * translates all pairs of opening-closing tag to LaTeX command. For example,
     * to make "''ital''" into "\emph{ital}" run <code>translateTag(wikicode,"\'\'","\'\'","\emph")</code>.
     * @param wikicode wikicode to translate
     * @param openTag open tag
     * @param closeTag close tag
     * @param latexCommand latex command like "\emph" or empty text if no command should
     * be used
     * @return translated wikicode
     */
    String translateTag(String wikicode, final String openTag, final String closeTag, final String latexCommand) {
        final int openLen = openTag.length();
        final int closeLen = closeTag.length();
        int begin = wikicode.indexOf(openTag);
        while (begin != -1) {
            int end = wikicode.indexOf(closeTag, begin+openLen);
            if (end == -1) {
                break;
            }
            String content = wikicode.substring(begin+openLen,end);
            
            if (!content.isEmpty() && !latexCommand.isEmpty()) {
                content = latexCommand + "{" + content + "}";
            }
            wikicode = wikicode.substring(0,begin) + content + wikicode.substring(end+closeLen);
            begin = wikicode.indexOf(openTag);
        }
        return wikicode;
    }

    /**
     * converts MediaWiki image to LaTeX image
     * @param wikicode fragment of wikicode containing only the image, starting with "[[Image:" and
     * ending with "]]" (or without this beginning and end)
     * @return LaTeX code
     */
    String convertImage(String wikicode) {
        wikicode = trimSection(wikicode, "[[Image:", "]]");

        //wikicode.replace("#$%^", "[[Image:");


        // TODO: open issue: transparent PNGs in pdflatex having black background

        String indent = settings.indent();
        boolean commentOut = false;
        if (settings.imageAction.equals(ConversionSettings.ImageAction.convertAndCommentOut)) {
            commentOut = true;
        }

        Pattern p = Pattern.compile("^(.+?)(\\.(png|PNG|svg|SVG|jpg|JPG|jpeg|JPEG|gif|GIF))");
        Matcher m = p.matcher(wikicode);

        if (!m.find()) {
            int length = wikicode.length() > 40 ? 40 : wikicode.length();
            logger.severe(String.format("cannot find title and extension for image %s", wikicode.substring(0, length)));
            return "";
        }

        String title = m.group(1);
        String extension = m.group(2);

        settings.addImages(title + extension);

        String label = title;

        String fullFilename = title + extension;
        fullFilename = Util.toSafeImageName(fullFilename);
        String fullPath = settings.imagepath + fullFilename;

        boolean thumb = false;
        if (wikicode.contains("thumb") || wikicode.contains("right") || wikicode.contains("left") || wikicode.contains("frame")) {
            thumb = true;
        }

        boolean center = false;
        if (wikicode.contains("center")) {
            center = true;
        }

        // TODO templates inside image captions can produce multi-line comments,
        // which break latex
        ArrayList<String> maskedLinks = new ArrayList<String>();
        ArrayList<String> maskedTemplates = new ArrayList<String>();
        wikicode = maskSections(wikicode, maskedLinks, "[[", "]]", "inimage");
        wikicode = maskSections(wikicode, maskedTemplates, "{{", "}}", "imtempl");
        String caption = "";
        for (int i = wikicode.length() - 1; wikicode.indexOf("|") != -1 && wikicode.charAt(i) != '|'; i--) {
            caption = wikicode.charAt(i) + caption;
        }
        wikicode = unmaskSections(wikicode, maskedTemplates, "imtempl");
        caption = unmaskSections(caption, maskedTemplates, "imtempl");
        wikicode = unmaskSections(wikicode, maskedLinks, "inimage");
        caption = unmaskSections(caption, maskedLinks, "inimage");


        if (caption.equals("thumb") || caption.equals("") || caption.equals("right") 
                || caption.equals("left") || caption.equals("center") || caption.contains("px")
                || caption.contains("frame")) {
            caption = null;
        }

        String size = "";
        if (wikicode.indexOf("px") > wikicode.indexOf("|")) {
            for (int index = wikicode.indexOf("px") - 1; wikicode.charAt(index) != '|'; index--) {
                size = wikicode.charAt(index) + size;
            }
            try {
                double sizeNum = (double) Integer.parseInt(size);
                sizeNum = (sizeNum / (double) settings.imageDPI) * 72.0;
                size = String.format(Locale.ENGLISH, "%2.1f", sizeNum);
            }
            catch (Exception e){
                size = "";
            }
        }

        // set image size
        String sizeSpec = "";
        if (!size.isEmpty()) {
            sizeSpec = "[width=" + size + "pt]";
        } 
        else if (thumb && !settings.noWrapFigs) {
            sizeSpec = "[width=" + settings.imageDefaultWidth + "]";
        }
        String includeGraphics = (commentOut ? "%" : "") + "\\includegraphics" + sizeSpec + "{" +
                fullPath + "}\n";

        String latex = "";

        // begin figure environment
        if (!isInsideTable) {
            if (thumb && !settings.noWrapFigs) {
                String wrapSize = size.isEmpty() ? settings.imageDefaultWidth : size + "pt";
                latex += "\n\\begin{wrapfigure}{R}{" + wrapSize + "}\n" +
                        indent + (commentOut ? "%" : "") + "\\begin{center}\n" +
                        indent + indent;
            } 
            else if (thumb){
                latex += "\\begin{figure}[htbp]" +
                        indent + (commentOut ? "%" : "") + "\\begin{center}\n" +
                        indent + indent;
            }
            else if (center) {
                latex += "\\begin{figure}[H]\n" +
                        indent + (commentOut ? "%" : "") + "\\begin{center}\n" +
                        indent + indent;
            } else {
                latex += "\\begin{figure}[H]\n" +
                        indent;
            }
        }

        // include graphics
        latex += includeGraphics;

        // end after includegraphics        
        if (!isInsideTable) {
            if (thumb && !settings.noWrapFigs) {
                latex += indent + (commentOut ? "%" : "") + "\\end{center}\n";
            }
            else if (thumb || center) {
                latex += indent + (commentOut ? "%" : "") + "\\end{center}\n";
            }
        }

        latex += indent; // caption and label
        if (!isInsideTable && caption != null) {
            latex += "\\caption{" + convert2(caption) + "}";
        }
        latex += "\\label{fig:" + label + "}\n";

        if (!isInsideTable) {
            if (thumb && !settings.noWrapFigs) {
                latex += "\\end{wrapfigure}\n";
            } else {
                latex += "\\end{figure}";
            }
        }

        return latex;
    }

    /**
     * converts internal links to LaTeX
     * @param wikicode MediaWiki wikicode
     * @return wikicode with links replaced with LaTeX hyperlinks
     */
    String convertInternalLinks(String wikicode) {
        // internal links
        int begin = wikicode.indexOf("[[");
        int middle = wikicode.indexOf("|", begin);
        int end = wikicode.indexOf("]]", begin);

        while (begin != -1 && end != -1) {
            String linkAddress, linkCaption, serverAddress;

            if (middle > begin && middle < end) { // if [[foo|bar]]
                linkAddress = wikicode.substring(begin + 2, middle);
                linkCaption = wikicode.substring(middle + 1, end);
            } else { // [[foo]]
                linkAddress = linkCaption = wikicode.substring(begin + 2, end);
            }

            linkAddress = linkAddress.trim().replace(" ", "_");
            linkCaption = linkCaption.trim().replace("_", " ");

            // wikipedia links
            if (linkAddress.startsWith("w:") || wikicode.startsWith("W:")) {
                serverAddress = bs.getLanguage() + ".wikipedia.org/wiki";
                linkAddress = linkAddress.substring(2);
            } else if (linkAddress.startsWith("wikipedia:") || wikicode.startsWith("Wikipedia:")) {
                serverAddress = bs.getLanguage() + ".wikipedia.org/wiki";
                linkAddress = linkAddress.substring("wikipedia:".length());
            } else if (linkAddress.startsWith("wikt:") || wikicode.startsWith("Wikt:")) {
                serverAddress = bs.getLanguage() + ".wiktionary.org/wiki";
                linkAddress = linkAddress.substring("wikt:".length());
            } else {
                serverAddress = bs.getServer() + ".org/wiki";
                linkAddress = linkAddress.replaceAll("^../", bs.title() + "/");
                linkCaption = linkCaption.replaceAll("^../", "");
            }

            // 2 is length of "[[" and "]]"
            wikicode = wikicode.substring(0, begin) + String.format("\\href{http://%s/%s}{%s}", serverAddress, linkAddress, linkCaption)
                    + wikicode.substring(end + 2);

            begin = wikicode.indexOf("[[");
            middle = wikicode.indexOf("|", begin);
            end = wikicode.indexOf("]]", begin);
        }
        return wikicode;
    }
    
    private String maskUrl(Map<String, String> maskedUrls, String url, int[] urlCount) {
        String masked = URL_MASK_PREFIX + ++urlCount[0];
        maskedUrls.put(masked, url);
        return masked;
    }

    private String unmaskUrls(String wikicode, Map<String, String> maskedUrls) {
        for (Map.Entry<String, String> pair : maskedUrls.entrySet()) {
            String url = pair.getValue();
            // revert converting LaTeX special characters
            url = url.replace("\\&","&").replace("\\_","_").replace("\\#","#");
            wikicode = wikicode.replace(pair.getKey(), url);
        }
        return wikicode;
    }

    String convertExternalLinks(String wikicode, Map<String, String> maskedUrls) {
        int[] urlCount = {0}; // hack to enable "pass by reference"

        // external links
        int begin = wikicode.indexOf("[http"); // was: "["
        if (begin == -1) {
            begin = wikicode.indexOf("[ftp");
        }
        if (begin == -1) {
            begin = wikicode.indexOf("[mailto");
        }
        if (begin == -1) {
            begin = wikicode.indexOf("[news");
        }
        int space = -1;
        int end = wikicode.indexOf("]", begin);
        while (begin != -1 && end != -1) {
            for (int i = begin + 1; i < end; i++) {
                // get index of space between foo and bar in [foo bar], if exists
                if (wikicode.charAt(i) == ' ') {
                    space = i;
                    break;
                }
            }

            if (!wikicode.substring(begin, end).contains("$")) {
                if (space != -1) { // [foo bar]
                    String url = wikicode.substring(begin + 1, space);
                    String desc = wikicode.substring(space + 1, end);
                    String link = url.equals(desc) ? String.format("\\url{%s}", maskUrl(maskedUrls, url, urlCount))
                            : String.format("\\href{%s}{%s}", maskUrl(maskedUrls, url, urlCount), desc);
                    wikicode = wikicode.substring(0, begin) + link +
                            (wikicode.length() > end + 1 ? wikicode.substring(end + 1) : "");
                } else { // [foo]
                    String url = wikicode.substring(begin + 1, end);
                    url = maskUrl(maskedUrls, url, urlCount);
                    wikicode = wikicode.substring(0, begin) + "\\url{" +
                            url + "}" +
                            (wikicode.length() > end + 1 ? wikicode.substring(end + 1) : "");
                }
            }

            begin = wikicode.indexOf("[");
            space = -1;
            end = wikicode.indexOf("]", begin);
        }

        // standalone links, like "see http://www.link.com/~foo for details"
        Pattern p = Pattern.compile("([ >\\(]|^|\\\\{2}|\\b)(http://[a-zA-Z0-9.\\-_/~?&:=\\\\#]+)([ \\)]|$)");
        while (true) {
            Matcher m = p.matcher(wikicode);
            if (!m.find()) {
                break;
            }
            String url = m.group(2);
            url = maskUrl(maskedUrls, url, urlCount);
            wikicode = wikicode.replace(m.group(), m.group(1) + "\\url{" + url + "}" + m.group(3));
        }

        return wikicode;
    }

    String convertInlineMath(String math) {
        math = trimSection(math, "<math>", "</math>");

        return "\\(" + (math.contains("\n") ? "\n" : "") + math + (math.contains("\n") ? "\n" : "") + "\\)";
    }

    String convertStandAloneMath(String math) {
        math = trimSection(math, "<standalone-math>", "</standalone-math>");

        return "\\[" + (math.contains("\n") ? "\n" : "") + math + (math.contains("\n") ? "\n" : "") + "\\]";
    }

    /**
     * converts MediaWiki <tt> to LaTeX code
     * @param text text beginning with <tt> and ending with </tt> (or without these tags)
     * @return LaTeX code
     */
    String convertTeletype(String text) {
        text = trimSection(text, "<tt>", "</tt>");

        /*char[] delimiters = {'+', '|', '!', 'I', '1', '&', '@', '-', '`', '_', '=', '(', 'q', ':'};
        int i=0;
        for(; i<delimiters.length; ++i) {
        if (!text.contains(Character.toString(delimiters[i])))
        break;
        }
        if (i < delimiters.length) {
        return String.format("\\verb%c%s%c", delimiters[i], text, delimiters[i]);
        } else { // oops!
        return "\\texttt{" + finalConvertLaTeXSpecialCharacters(convertLaTeXSpecialCharacters(text)) + "}";
        }*/
        //text = finalConvertLaTeXSpecialCharacters(convertLaTeXSpecialCharacters(text));
        text = convertInlineFormatting(text, true);
        return "\\texttt{" + text + "}";
    }

    /**
     * converts &lt;center&gt; tag
     * @param wikicode
     * @return
     */
    String convertCenter(String wikicode) {
        wikicode = trimSection(wikicode, "<center>", "</center>");

        return "\n\\begin{center}\n" + wikicode + "\n\\end{center}\n";
    }

    /**
     * converts HTML comment to LaTeX comment.
     * @param comment text of comment, with or without opening/closing <code>&lt;!--</code>
     * and <code>--&gt;</code> tags
     * @return LaTeX comment
     */
    String convertComment(String comment) {
        comment = trimSection(comment, "<!--", "-->");
        boolean multiline = comment.contains("\n");

        // Text after "\end{comment}" must go on the next line in order for it
        // to appear.

        return String.format(multiline ? "\n\\begin{comment}\n%s\n\\end{comment}" : "\\begin{comment} %s \\end{comment}", comment);
    }

    String convertNoinclude(String text) {
        return "";
    }

    String convertIncludeonly(String text) {
        text = trimSection(text, "<includeonly>", "</includeonly>");
        return convert2(text);
    }

    String convertNowikiSection(String text) {
        text = trimSection(text, "<nowiki>", "</nowiki>");
        text = finalConvertLaTeXSpecialCharacters(convertHTMLEntities(convertLaTeXSpecialCharacters(text)));
        return text;
    }

    String convertPreformattedText(String wikicode) {
        wikicode = trimSection(wikicode, "<pre>", "</pre>");

        //boolean multiline = (wikicode.contains("\n")) ? true : false;
        boolean multiline = true;
        boolean listing = (settings.convertPreToCodeListing && settings.codeLanguage() != null) ? true : false;

        // set environment/command to use
        String latex = "\\verb|__TEXT__|";
        if (listing && multiline) {
            latex = "\n\\begin{lstlisting}\n__TEXT__\\end{lstlisting}\n";
        } else if (listing) {
            latex = "\\lstinline|__TEXT__|";
        } else if (multiline) {
            latex = "\n\\begin{verbatim}\n__TEXT__\\end{verbatim}\n";
        }

        String text = "";
        for (String line : Util.lineArray(wikicode)) {
            // if indented, convert tab to spaces
            if (line.length() > 0 && line.charAt(0) == '\t') {
                line = (multiline ? "    " : "") + line.substring(1, line.length());
            }

            text += (multiline ? settings.indent() : "") + line + (multiline ? "\n" : "");
        }

        if (text.contains("|") && latex.contains("|")) // fix conflict with \verb|...| or \lstinline|...|
        {
            latex = latex.replace("|", "!");
        }

        return latex.replace("__TEXT__", text);
    }

    String convertSource(String wikicode) {
        int langIndex = wikicode.indexOf("\"");
        String language = wikicode.substring(langIndex + 1, wikicode.indexOf(
                "\"", langIndex + 1));
        String openTag = wikicode.substring(wikicode.indexOf("<source"),
                wikicode.indexOf("\">") + 2);

        wikicode = trimSection(wikicode, openTag, "</" + "source>");
        return convertSource(wikicode, language);
    }

    /**
     * converts <code>&lt;source&gt;</code> tag
     * @param wikicode MediaWiki markup
     * @param language language code given in <emph>lang</emph> param of
     * <code>&lt;source&gt;</code> tag
     * @return wikicode with <code>&lt;source&gt;</code> tag replaced by
     * LaTeX code listing markup
     */
    String convertSource(String wikicode, String language) {
        wikicode = trimSection(wikicode, "<source lang=\"" + language + "\">", "</" + "source>");

        if (!settings.convertPreToCodeListing && settings.convertCodeToPre) {
            return convertPreformattedText(wikicode);
        }

        // set usable language, if necessary
        language = Util.getValidCodeLanguage(language);
        if (language != null && (settings.codeLanguage() == null || !settings.codeLanguage().equals(language))) {
            language = "[language={" + language + "}]";
        } else {
            language = "";
        }

        boolean multiline = (wikicode.contains("\n")) ? true : false;

        if (multiline) {
            wikicode = wikicode.replace("\t", "    ");
            wikicode = wikicode.replace("\n", "\n" + settings.indent());
            wikicode = settings.indent() + wikicode;
            return "\\begin{lstlisting}" + language + "\n" + wikicode + "\n\\end{lstlisting}";
        } else if (!wikicode.contains("|")) {
            return "\\lstinline" + language + "|" + wikicode + "|";
        } else {
            return "\\lstinline" + language + "!" + wikicode + "!";
        }
    }

    String convertCode(String wikicode) {
        wikicode = trimSection(wikicode, "<code>", "</code>");
        return convertTeletype(wikicode);

    /*
    //was: wikicode = convert2(wikicode);
    wikicode = convertInlineFormatting(" "+wikicode); // hack to prevent seeing as begin of line
    wikicode = wikicode.trim();
    boolean listing = (settings.convertCodeToCodeListing && settings
    .codeLanguage() != null) ? true : false;
    if (listing)
    return convertSource(wikicode, settings.codeLanguage());
    boolean multiline = (wikicode.contains("\n")) ? true : false;
    if (multiline) {
    wikicode = wikicode.replace("\t", "    ");
    wikicode = wikicode.replace("\n", "\n" +  settings.indent());
    wikicode = settings.indent() + wikicode;
    return "\\texttt{\n" + wikicode + "\n}";
    } else
    return "\\texttt{" + wikicode + "}";
     */
    }

    /**
     * Handles templates in this order:
     * 
     * <ol>
     * <li> Templates in templateMap are converted to the mapped LaTeX command.</li>
     * <li> Else, templates found in <code>convertSpecificTemplate(String title,
     * String... parameters)</code>
     * are handled by that method.</li>
     * <li> Else, templates are either commented out (default), ignored (left in
     * LaTeX markup, or removed. This default action is set with
     * <code>setTemplateAction(String action)</code></li>
     * 
     * @param wikicode
     *            MediaWiki-markup formatted template (ie {{foo|bar}})
     * @return String which will appear in LaTeX output.
     */
    String convertTemplate(String wikicode) {
        //wikicode = "{{" + trimSection(wikicode, "{{", "}}") + "}}";
        ConversionSettings.TemplateAction action = settings.templateAction;

        int argsBegin = wikicode.indexOf("|");
        String title;
        if (argsBegin != -1) {
            title = wikicode.substring(2, argsBegin);
            ++argsBegin; // move after |
        } else {
            title = wikicode.substring(2, wikicode.lastIndexOf("}}"));
        }
        title = title.trim();

        if (title.contains("\n")) {
            title = title.substring(0, title.indexOf("\n"));
            argsBegin = wikicode.indexOf("\n") + "\n".length();
        }

        //String[] arguments = getTemplateArguments(wikicode);
        String[] arguments = getTemplateArguments(wikicode, null);

        String internalWikicode = argsBegin != -1
                ? wikicode.substring(argsBegin, wikicode.lastIndexOf("}}"))
                : "";

        // specific templates
        if (title.equalsIgnoreCase("porada") || title.equalsIgnoreCase("uwaga") || title.equalsIgnoreCase("infobox")) {
            String environmentName = title.toLowerCase();
            internalWikicode = internalWikicode.replaceAll("\\s*1=", "");
            return String.format("\n\\begin{%s}\n%s\n\\end{%s}\n", environmentName,
                    convert2(internalWikicode), environmentName);
        }

        if (title.equalsIgnoreCase("Python/Src")) {
            action = ConversionSettings.TemplateAction.removeExceptFirstargument;
        }
        //end specific templates


        String latex = convertSpecificTemplate(title, arguments);
        if (latex != null) {
            return latex;
        } else if (settings.templateMap().containsKey(title)) {
            return convertTemplateToCommand(title, arguments);
        } else if (action.equals(ConversionSettings.TemplateAction.ignore)) {
            return wikicode.replace("{", "\\{").replace("}", "\\}");
        } else if (action.equals(ConversionSettings.TemplateAction.remove)) {
            return "";
        } else if (action.equals(ConversionSettings.TemplateAction.removeExceptFirstargument)) {
            return arguments[0];
        } else if (action.equals(ConversionSettings.TemplateAction.commentOut)) {
            // comment out multiline templates                        
            String retval = "\n% commented out template(" + title + ")\n";
            if (!internalWikicode.equals("")) {
                String inside = convert2(internalWikicode);
                retval += "\\begin{comment}\n" + inside +
                        "\n\\end{comment}\n";
            }
            return retval;
        } else if (action.equals(ConversionSettings.TemplateAction.convertToCommand)) {
            if (!settings.templateMap().containsKey(title)) {
                settings.addToTemplateMap(title, title);
            }
            return convertTemplateToCommand(title, arguments);
        } else // expand template
        {
            return expandTemplate(title, arguments);
        }
    }

    @Deprecated
    String[] getTemplateArguments( String line) {
        ArrayList<String> arguments = new ArrayList<String>(); // template arguments in order

        int begin = line.indexOf("|");
        int end = ((line.indexOf("|", begin + 1) != -1) ? line.indexOf("|", begin + 1) : line.indexOf("}}"));
        while (begin != -1 && end != -1) {
            arguments.add(line.substring(begin + 1, end));

            begin = line.indexOf("|", begin + 1);
            end = line.indexOf("|", begin + 1) != -1 ? line.indexOf("|", begin + 1) : line.indexOf("}}");
        }
        return arguments.toArray(new String[]{});
    }

    /**
     * gets arguments of MediaWiki template.
     * @param wikicode MediaWiki markup starting with {{ and ending with }}
     * @param namedArguments if not null, named argumnents will be added here
     * as (Name=>Value) pairs
     * @return values of all arguments. In named arguments, line <code>foo=bar</code>, only
     * <code>bar</code> is added.
     */
    String[] getTemplateArguments( String wikicode,  Map<String, String> namedArguments) {
        wikicode = wikicode.trim();
        if (!wikicode.startsWith("{{") || !wikicode.endsWith("}}")) {
            final int firstCharacters = 5;
            String codeBegin = wikicode.length() >= firstCharacters ? wikicode.substring(0, firstCharacters) : wikicode;
            logger.info(logPrefix + String.format("template '%s' does not start", codeBegin));
            return new String[0];
        }
        int len = wikicode.length();
        // strip name of the template
        int i = 1;
        for (; i < len && wikicode.charAt(i) != '|' && wikicode.charAt(i) != '}'; ++i) {
        }
        if (wikicode.charAt(i) == '|') {
            ++i;
        }
        int newLineIndex = wikicode.substring(0, i).indexOf("\n");
        if (newLineIndex != -1) {
            i = newLineIndex + "\n".length();
        }
        wikicode = wikicode.substring(i, wikicode.lastIndexOf("}}"));

        final String MASK = "templatesec";
        ArrayList<String> internalTemplates = new ArrayList<String>();
        wikicode = maskSections(wikicode, internalTemplates, "{{", "}}", MASK);

        String[] parts = wikicode.split("\\|");
        if (parts.length == 1 && parts[0].trim().equals("|")) {
            return new String[0];
        }

        ArrayList<String> arguments = new ArrayList<String>();
        for (String arg : parts) {
            arg = arg.replaceAll("^ +| +$", ""); // remove spaces from begin and end                
            arg = arg.replaceAll("^(\\n|\\r)+|(\\n|\\r)+$", "");
            if (arg.equals("")) {
                continue;
            }

            int eqIndex = arg.indexOf("=");
            if (eqIndex != -1 && eqIndex != 0) { // cannot be first letter, arg name must not be empty
                String argName = arg.substring(0, eqIndex);
                String argVal = eqIndex < arg.length() ? arg.substring(eqIndex + 1) : "";

                argVal = unmaskSections(argVal, internalTemplates, MASK);
                if (namedArguments != null) {
                    namedArguments.put(argName, argVal);
                }
                arguments.add(argVal);
            } else {
                arg = unmaskSections(arg, internalTemplates, MASK);
                arguments.add(arg);
            }
        }
        return arguments.toArray(new String[]{});
    }

    /**
     * Converts specific templates to LaTeX markup. Add code here to deal with
     * specific templates.
     * 
     * @param title
     *            The template's name. "foo" in {{foo}} or {{foo|bar|baz}}
     * @param arguments
     *            An array of the template's parameters. Empty in {{foo}},
     *            parameters[0] is "bar" and parameters[1] is "baz" in
     *            {{foo|bar|baz}}
     * 
     * @return The LaTeX formatted markup, or null if the specific template
     *         isn't here.
     */
    String convertSpecificTemplate(String title, String... arguments) {
        if (title.equalsIgnoreCase("Python/Przykład")) {
            if (arguments.length < 3) {
                String argsText = arguments.length > 0 ? arguments[0] : "(empty)";
                logger.severe(String.format("%swrong number " +
                        "of arguments for Python/Przykład: %d\n%s\n", logPrefix, arguments.length,
                        argsText));
                return null;
            } else {
                StringBuilder internalSB = new StringBuilder();
                for (int i = 2; i < arguments.length; ++i) {
                    internalSB.append(arguments[i]);
                    if (i < arguments.length - 1) {
                        internalSB.append("|");
                    }
                }
                String internal = convert2(internalSB.toString());
                return String.format("\n\\begin{pythonPrzyklad}{%s}{%s}\n%s\n\\end{pythonPrzyklad}\n",
                        arguments[0], convert2(arguments[1]), internal);
            }
        } else if (title.equalsIgnoreCase("Keyword")) { // example for
            // {{Keyword|key}}
            return "\\emph{" + arguments[0] + "}"; // emphasize keywords

        } else if (title.equalsIgnoreCase("TOCright") || title.contains("Nav") || title.equals("-")) {
            return ""; // remove {{TOCright}}, {{-}} and navigation templates

        } else if (title.equalsIgnoreCase("wikipedia")) { // {{Wikipedia|foo}}
            return "\\wikipedia{" + arguments[0].replace(" ", "_") + "}{" + arguments[0] + "}";

        } else if (title.equalsIgnoreCase("sidebox")) { // {{sidebox|foo}}
            return "\\sidebox{" + convert2(arguments[0].trim()) + "}";

        } else if (title.equalsIgnoreCase("textbox")) { // {{textbox|foo}}
            return "\\textbox{" + convert2(arguments[0].trim()) + "}";
        } else if (title.equalsIgnoreCase("seealso")) { // {{seealso|foo}}
            return "\\marginpar{\\textit{See also: \\href{http://" + bs.getServer() + ".org/wiki/"
                    + arguments[0] + "}{" + arguments[0] + "}}}";

        } else // all other templates
        {
            return null;
        }
    }

    String convertTemplateToCommand(String title, String... arguments) {
        if (settings.templateMap().containsKey(title)) {
            String latex = "\\" + settings.templateMap().get(title);
            for (String parameter : arguments) {
                latex += "{" + parameter + "}";
            }
            return latex;
        } else {
            return null;
        }
    }

    String expandTemplate(String title, String... arguments) {
        String template = Util.downloadTemplate(title);
        template = convert(template);
        template = Util.convertTemplateArguments(template);

        for (int i = 1; i < 10; i++) {
            template.replace("#" + i, arguments[i]);
        }

        return template;
    }

    /**
     * Converts to LaTeX MediaWiki markup of the form:
     * 
     * <p>
     * <code>
     * &lt;gallery><br />
     * Image:foo.jpg<br />
     * Image:foo.jpg|bar<br />
     * &lt;/gallery>
     * </code>
     * </p>
     * 
     * @param wikicode
     *            MediaWiki markup of the form shown above.
     * @return LaTeX markup.
     */
    String convertGallery(String wikicode) {
        wikicode = wikicode.replace("\n<gallery>\n", "").replace(
                "\n</gallery>\n", "").replace("<gallery>\n", "").replace(
                "\n</gallery>", "");
        String indent = settings.indent();
        boolean commentOut = false;
        if (settings.imageAction.equals(ConversionSettings.ImageAction.convertAndCommentOut)) {
            commentOut = true;
        }

        String latex = "\\begin{figure}[!h]\n" + indent + "\\centering\n";

        for (String line : Util.lineArray(wikicode)) {
            if (line.matches("(I|i)mage:(.)+\\.([a-zA-Z]){3}(\\|)?((.)?)+")) {
                String title = line.substring(6, line.indexOf("."));

                String extension = line.substring(line.indexOf('.'), line.indexOf('.') + 4);
                settings.addImages(title + extension);

                String caption = null;
                if (line.contains("|")) {
                    caption = convert2(line.substring(line.indexOf("|") + 1));
                }
                latex += indent + "\\subfloat" + (caption != null ? ("[" + convert2(caption) + "]") : "") + "{\n"
                        + indent + indent + (commentOut ? "%" : "")
                        + "\\includegraphics[width=0.3\\textwidth]{" + settings.imagepath + title + "}\n"
                        + indent + "}\\quad\n";
            } else {
                Log.addError(logPrefix + "Could not add \"" + line + "\" to gallery. Invalid syntax.");
            }
        }

        latex += "\\end{figure}";
        return latex;
    }

    /**
     * Converts to LaTeX MediaWiki markup of the form:
     * 
     * <p>
     * <code>
     * {|<br />
     * |+ caption<br />
     * | a || b || colspan="2" | c<br />
     * | colspan="2" | d<br />
     * |-<br />
     * !e<br />
     * | f<br />
     * |}
     * </code>
     * </p>
     * 
     * @param wikicode
     *            MediaWiki markup of the form shown above.
     * @return LaTeX markup.
     */
    String convertTable(String wikicode) {
        wikicode = wikicode.replace("\n{|\n", "").replace("\n|}\n", "").replace("{|\n", "").replace("\n|}", "");

        boolean withBorder = false;
        int firstLineEnd = wikicode.indexOf("\n");
        String firstLine = "";
        if (firstLineEnd > 0) {
            firstLine = wikicode.substring(0, firstLineEnd);
        }
        if (firstLine.contains("wikitable") || firstLine.contains("prettytable")
                || (firstLine.contains("border=") && !firstLine.contains("border=0") && !firstLine.contains("border=\"0\""))) {
            withBorder = true;
            // remove first line
            wikicode = wikicode.substring(firstLineEnd+"\n".length());
        }

        // caption
        String caption = null;
        int captionIndex = wikicode.indexOf("|+");
        if (captionIndex != -1) {
            int endOfCaptionIndex = wikicode.indexOf("\n", captionIndex);
            caption = wikicode.substring(captionIndex + 2, endOfCaptionIndex).trim();
            wikicode = wikicode.substring(0, captionIndex) + wikicode.substring(endOfCaptionIndex + 1);
        }

        final String indent = settings.indent();
        StringBuilder latex = new StringBuilder("\\begin{table}[H]\n");
        latex.append(indent).append("\\begin{tabular}{"); // tabularx for wrappable tables
        if (withBorder) {
            latex.append("|");
        }
        latex.append("__COLUMNSTEXT__}\n");
        if (withBorder) {
            latex.append(indent + indent).append("\\hline\n");
        }

        int maxColumns = 1;
        BitSet wrapColumns = new BitSet();

        for (String row : tableRowArray(wikicode)) {
            
            String[] cells = tableCellArray(row);
            latex.append(indent + indent);

            if (cells.length > maxColumns) {
                maxColumns = cells.length;
            }

            for (int i = 0; i < cells.length; ++i) {

                int len = cells[i].length(); // measure wikicode length
                cells[i] = convertTableCell(cells[i]);
                
                if (containsWrapSection(cells[i]) || len > settings.textWidth){
                    wrapColumns.set(i);
                }
                latex.append(cells[i]).append(" & ");
            }
            final int ampIndex = latex.lastIndexOf("&");
            if (ampIndex != -1) {
                latex = latex.delete(ampIndex, latex.length());
            }
            latex.append("\\\\");
            if (withBorder) {
                latex.append(" \\hline");
            }
            latex.append("\n");
        }

        // compute column widths
        String wrapColWidth = String.format(Locale.ENGLISH, "%3.2f", wrapColumns.cardinality() * 2.0 / maxColumns)
                + "\\textwidth";
        StringBuilder columnsText = new StringBuilder();
        for (int i = 0; i < maxColumns; i++) {
            
            if (wrapColumns.get(i)){
                columnsText.append(" p{").append(wrapColWidth).append("} ");
            }
            else {
                columnsText.append(" c");
            }
            if (withBorder) {
                columnsText.append(" |");
            }
        }

        latex.append(indent).append("\\end{tabular}\n");
        latex.append((caption != null ? indent + "\\caption{" + convert2(caption) + "}\n" : ""));
        latex.append("\\end{table}\n");

        String latexStr = latex.toString();
        latexStr = latexStr.replace("__COLUMNSTEXT__", columnsText);

        return latexStr;
    }

    /**
     * Converts String lines to an array of rows.
     * 
     * @param lines
     *            multiline String of wikitable syntax
     * @return Array of lines in String lines
     */
    private String[] tableRowArray(String lines) {
        if (lines == null) {
            return null;
        }

        ArrayList<String> rows = new ArrayList<String>();

        while (lines.length() > 0) {
            int newRowIndex = lines.indexOf("|-");
            if (newRowIndex == -1) {
                newRowIndex = lines.length();
            }
            String row = lines.substring(0, newRowIndex);
            if (row.length() > 0) {
                if (row.charAt(0) == '\n') {
                    row = row.substring(1);
                }
                if (row.length() > 1 && row.charAt(row.length() - 1) == '\n') {
                    row = row.substring(0, row.length() - 1);
                }
                rows.add(row);
            }
            if (newRowIndex < lines.length() - 2) {
                lines = lines.substring(newRowIndex + 2);
            } else {
                lines = "";
            }
        }
        return rows.toArray(new String[]{});
    }

    /**
     * Given a wikicode syntax row, it returns an array of cells contained in this row (still in wikicode)
     * @param row the input wikicode text
     * @return an array of cells' texts (still in wikicode)
     */
    private String[] tableCellArray(String row) {

        if (row == null) {
            return null;
        }

        ArrayList<String> cells = new ArrayList<String>();

        for (String line : Util.lineArray(row)) {
            if (line.length() > 0 && line.matches("(!|\\|).*")) {
                // if "| foo || bar"
                if (line.contains("||") || line.contains("!!")) {
                    String type = line.substring(0, 1); // remove ! or | from the beginning
                    line = line.substring(1);
                    String[] parts;
                    if (line.contains("!!")) {
                        parts = line.split("!!");
                    } else {
                        parts = line.split("\\|\\|");
                    }
                    for (String part : parts) {
                        cells.add(type + part);
                    }
                } else {
                    // if "|foo" or "| foo | bar" or "!foo |bar"
                    cells.add(line);
                }
            } else {
                // if text, added to last line
                if (cells.size() > 0) {
                    cells.add(cells.remove(cells.size() - 1) + line);
                }
            }
        /*if (line.length() > 0 && line.matches("(!|\\|)([^(!!)(||)])*")) {
        // if "|foo" or "| foo | bar" or "!foo |bar"
        cells.add(line);
        } else if (line.length() > 0 && line.matches("(!|\\|)(.)*")) {
        // if "| foo || bar"
        String type = "" + line.charAt(0); // ! or |
        int begin = 1;
        int end = line.indexOf("||", begin);
        int altEnd = line.indexOf("!!", begin);
        if (altEnd != -1 && altEnd < end) {
        end = altEnd;
        }
        while (begin != -1 && end != -1) {
        cells.add(type + line.substring(begin, end));
        begin = (line.length() > end + 2) ? end + 2 : -1;
        end = line.indexOf("||", begin);
        altEnd = line.indexOf("!!", begin);
        if (altEnd != -1 && (altEnd < end || end == -1)) {
        end = altEnd;
        }
        }
        if (begin > -1) {
        cells.add(type + line.substring(begin));
        } // last cell
        } else if (line.length() == 0 || (line.charAt(0) != '|' && line.charAt(0) != '!')) {
        // if text, added to last line
        if (cells.size() > 0) {
        cells.add(cells.remove(cells.size() - 1) + line);
        }
        }*/
        }

        return cells.toArray(new String[]{});
    }

    /**
     * 
     * 
     * @param cell
     *            "|foo" or "| foo | bar" or "! foo | bar"
     * @return LaTeX markup for this table cell
     */
    private String convertTableCell(String cell) {
        boolean title = false;
        if (cell.charAt(0) == '!') {
            title = true;
        }
        cell = cell.substring(1);

        ArrayList<String> masked = new ArrayList<String>();
        final String maskPrefix = "cell";
        cell = maskSections(cell, masked, "[[", "]]", maskPrefix);

        String text = cell.substring(cell.indexOf('|') + 1);

        cell = unmaskSections(cell, masked, maskPrefix);
        text = unmaskSections(text, masked, maskPrefix);

        text = text.trim();
        
        isInsideTable = true;

        //was: text = convert2(text);
        //was: convertInlineFormatting               
        text = convert2("&nbsp;" + text); // hack to prevent treating this as new line

        isInsideTable = false;
        
        if (title) {
            text = "\\textbf{" + text + "}";
        }

        if (cell.contains("colspan=\"")) {
            char columns = cell.charAt(cell.indexOf("colspan=\"") + 9);
            return "\\multicolumn{" + columns + "}{|c|}{" + text + "}";
        } else if (cell.contains("colspan=")) {
            char columns = cell.charAt(cell.indexOf("colspan=") + 8);
            return "\\multicolumn{" + columns + "}{|c|}{" + text + "}";
        } else {
            return text;
        }
    }

    String convertIndentedPreformattedText(String wikicode) {
        String sectionText = "\\begin{verbatim}\n";
        for (String line : Util.lineArray(wikicode)) {
            sectionText += "\t" + stripWikiFormatting(line.substring(1)) + "\n";
        }
        sectionText += "\\end{verbatim}";

        return sectionText;
    }

    /**
     * removes MediaWiki markup from given string.
     * <p>For example:
     * <pre>#include <[[C/Libraries#stdio.h|stdio.h]]>
     *{{wrong|void main() { }}
     *  '''int''' a;
     *}</pre> becomes
     * <pre>#include <stdio.h>
     *void main() { 
     *   int a;
     *}</pre></p>
     * @param wikicode string with MediaWiki markup
     * @return the same string with without MediaWiki markup
     */
    String stripWikiFormatting(String wikicode) {
        wikicode = wikicode.replaceAll("</?(nowiki|code|span|tt)>", "");
        wikicode = wikicode.replaceAll("\\{\\{[^\\|\\}]+(\\|(1=)?)?", "");
        wikicode = wikicode.replace("}}", "");
        wikicode = wikicode.replaceAll("\\[\\[([^\\|]+\\|)?([^\\]]+)\\]\\]", "$2");
        wikicode = wikicode.replace("'''", "");
        wikicode = wikicode.replace("''", "");
        return wikicode;
    }

    String convertIndentedText(String wikicode) {
        String latex = "\n\\begin{description}\n";
        for (String line : Util.lineArray(wikicode)) {
            int index = 0;
            for (; line.length() > index && line.charAt(index) == ':'; index++) {                
            }
            latex += "\\item ";
            latex += convert2(line.substring(index).trim()) + "\n";
        }
        return latex + "\\end{description}\n";
    }

    String convertList(String wikicode) {
        Scanner listIterator = new Scanner(wikicode + "\n\n");
        String latex = "";
        String line = listIterator.nextLine();

        if (wikicode.charAt(0) == '#') {
            // text[] = {converted list code, unconverted current line};
            String[] text = convertList(line, 0, true, listIterator);
            latex = text[0];
        } else if (wikicode.charAt(0) == '*') {
            String[] text = convertList(line, 0, false, listIterator);
            latex = text[0];
        } else if (wikicode.charAt(0) == ';') {
            String[] text = convertDescriptionList(line, listIterator);
            latex = text[0];
        } else {
            latex = convertInlineFormatting(wikicode, true);
        }

        while (latex.length() > 1 && latex.charAt(latex.length() - 2) == '\n') // remove extra newlines
        {
            latex = latex.substring(0, latex.length() - 1);
        }

        listIterator = null;
        return latex;
    }

    private String[] convertList(String line, int index, boolean ordered, Scanner listIterator) {

        char listChar = ordered ? '#' : '*';
        boolean first = true; // used for correct adding of nested items at the beginning

        // index is used for correct indentation - index = 0 means no indents, etc
        String latex = "";
        for (int i = 0; i < index; i++) {
            latex += settings.indent();
        }

        latex += ordered ? "\\begin{enumerate}\n" : "\\begin{itemize}\n";

        loop:
        for (; line.length() > index + 1 && line.charAt(index) == listChar; line = listIterator.nextLine()) {

            // first, check if deeper lists exist and do them recursively
            for (int i = 0; i < 2; i++) { // do twice to allow both ordered and unordered lists on the same level
                if (line.length() > index + 2 && line.charAt(index + 1) == '#') {
                    String[] returned = convertList(line, index + 1, true, listIterator);
                    latex += (first ? "\\item " : "") + returned[0];
                    line = returned[1];
                }
                if (line.length() > index + 2 && line.charAt(index + 1) == '*') {
                    String[] returned = convertList(line, index + 1, false, listIterator);
                    latex += (first ? "\\item " : "") + returned[0];
                    line = returned[1];
                }
            }

            // next, add the item at the present index level if it exists
            if (line.length() > 1 && line.charAt(index) == listChar) {
                for (int i = 0; i < index + 1; i++) {
                    latex += settings.indent();
                }
                latex += "\\item " + trim(convert2(trim(line.substring(index + 1)))) + "\n";
            }
            else if (!listIterator.hasNextLine()) {
                line = "";
                break loop;
            } else {
                break loop;
            }

            first = false;
        }

        for (int i = 0; i < index; i++) {
            latex += settings.indent();
        }

        latex += ordered ? "\\end{enumerate}\n" : "\\end{itemize}\n";;
        return new String[]{latex, line};
    }


    private String[] convertDescriptionList(String line, Scanner listIterator) {
        String latex = "\\begin{description}\n";

        while (listIterator.hasNextLine() && line.length() > 1 && line.charAt(0) == ';') {
            String word = "", definition = "";

            if (line.contains(":")) {
                word = trim(convert2(trim(line.substring(1, line.indexOf(":")))));
                definition = trim(convert2(trim(line.substring(line.indexOf(':') + 1))));
                line = listIterator.nextLine();
            } else {
                word = convert2(line.substring(1).trim());
                line = listIterator.nextLine();
                if (listIterator.hasNextLine() && line.length() > 1 && line.charAt(0) == ':') {
                    definition += trim(convert2(trim(line.substring(1))));
                    line = listIterator.nextLine();
                }
                while (listIterator.hasNextLine() && line.length() > 1 && line.charAt(0) == ':') {
                    definition += " \\\\ " + trim(convert2(trim(line.substring(1))));
                    line = listIterator.nextLine();
                }
            }
            latex += settings.indent() + "\\item[" + word + "] " + definition + "\n";
        }

        latex += "\\end{description}\n";
        return new String[]{latex, line};
    }


    /**
     * Converts various HTML entities (Greek letters, etc.) to their LaTeX counterparts.
     * @param wikicode
     * @return the code, with HTML entities replaced
     */
    private String convertHTMLEntities(String wikicode) {
        
        final String [] [] entitiesRepl = new String [] [] {
            {"gt", "\\textgreater{}"},
            {"lt", "\\textless{}"},
            {"amp", "\\&"},
            {"quot", "\""},
            {"alpha", "\\ensuremath{\\alpha}"},
            {"beta", "\\ensuremath{\\beta}"},
            {"gamma", "\\ensuremath{\\gamma}"},
            {"delta", "\\ensuremath{\\delta}"},
            {"epsilon", "\\ensuremath{\\varepsilon}"},
            {"zeta", "\\ensuremath{\\zeta}"},
            {"eta", "\\ensuremath{\\eta}"},
            {"theta", "\\ensuremath{\\theta}"},
            {"iota", "\\ensuremath{\\iota}"},
            {"kappa", "\\ensuremath{\\kappa}"},
            {"lambda", "\\ensuremath{\\lambda}"},
            {"mu", "\\ensuremath{\\mu}"},
            {"nu", "\\ensuremath{\\nu}"},
            {"xi", "\\ensuremath{\\xi}"},
            {"omicron", "\\ensuremath{\\omicron}"},
            {"pi", "\\ensuremath{\\pi}"},
            {"rho", "\\ensuremath{\\rho}"},
            {"sigma", "\\ensuremath{\\sigma}"},
            {"tau", "\\ensuremath{\\tau}"},
            {"upsilon", "\\ensuremath{\\upsilon}"},
            {"phi", "\\ensuremath{\\phi}"},
            {"chi", "\\ensuremath{\\chi}"},
            {"psi", "\\ensuremath{\\psi}"},
            {"omega", "\\ensuremath{\\omega}"},
            {"le", "\\ensuremath{\\leq}"},
            {"ge", "\\ensuremath{\\geq}"},
        };

        for (String [] entityRepl : entitiesRepl){
            wikicode = wikicode.replace("\\&" + entityRepl[0] + ";", entityRepl[1]);
        }
        return wikicode;
    }
}
